#[cfg(feature = "azure-kms")]
#[path = "csfle/azure_imds.rs"]
mod azure_imds; // requires mock IMDS server
#[cfg(feature = "openssl-tls")]
#[path = "csfle/kmip.rs"]
mod kmip; // requires KMIP server
#[cfg(not(feature = "openssl-tls"))]
#[path = "csfle/kms_retry.rs"]
mod kms_retry; // requires mock HTTP server
#[cfg(feature = "aws-auth")]
#[path = "csfle/on_demand_aws.rs"]
mod on_demand_aws; // requires AWS credentials to be set or unset
#[cfg(feature = "gcp-kms")]
#[path = "csfle/on_demand_gcp.rs"]
mod on_demand_gcp; // requires GCP
#[path = "csfle/prose.rs"]
mod prose; // requires environment variables listed below
#[path = "csfle/spec.rs"]
mod spec; // requires environment variables listed below

use std::{env, path::PathBuf};

use crate::bson::{doc, Document, RawBson};
use anyhow::Context;
use mongocrypt::ctx::{Algorithm, KmsProvider, KmsProviderType};
use std::sync::LazyLock;

use crate::{
    base64,
    client_encryption::{ClientEncryption, EncryptKey},
    options::{CollectionOptions, ReadConcern, TlsOptions, WriteConcern},
    Client,
    Collection,
    Namespace,
};

use super::{log_uncaptured, server_version_lt, topology_is_standalone, EventClient};

type Result<T> = anyhow::Result<T>;
pub(crate) type KmsInfo = (KmsProvider, Document, Option<TlsOptions>);
pub(crate) type KmsProviderList = Vec<KmsInfo>;

// The environment variables needed to run the CSFLE tests. These values can be retrieved from the
// AWS secrets manager by running the setup-secrets.sh script in drivers-evergreen-tools.
static CSFLE_LOCAL_KEY: LazyLock<String> = LazyLock::new(|| get_env_var("CSFLE_LOCAL_KEY"));
static FLE_AWS_KEY: LazyLock<String> = LazyLock::new(|| get_env_var("FLE_AWS_KEY"));
static FLE_AWS_SECRET: LazyLock<String> = LazyLock::new(|| get_env_var("FLE_AWS_SECRET"));
static FLE_AWS_TEMP_KEY: LazyLock<String> =
    LazyLock::new(|| get_env_var("CSFLE_AWS_TEMP_ACCESS_KEY_ID"));
static FLE_AWS_TEMP_SECRET: LazyLock<String> =
    LazyLock::new(|| get_env_var("CSFLE_AWS_TEMP_SECRET_ACCESS_KEY"));
static FLE_AWS_TEMP_SESSION_TOKEN: LazyLock<String> =
    LazyLock::new(|| get_env_var("CSFLE_AWS_TEMP_SESSION_TOKEN"));
static FLE_AZURE_TENANTID: LazyLock<String> = LazyLock::new(|| get_env_var("FLE_AZURE_TENANTID"));
static FLE_AZURE_CLIENTID: LazyLock<String> = LazyLock::new(|| get_env_var("FLE_AZURE_CLIENTID"));
static FLE_AZURE_CLIENTSECRET: LazyLock<String> =
    LazyLock::new(|| get_env_var("FLE_AZURE_CLIENTSECRET"));
static FLE_GCP_EMAIL: LazyLock<String> = LazyLock::new(|| get_env_var("FLE_GCP_EMAIL"));
static FLE_GCP_PRIVATEKEY: LazyLock<String> = LazyLock::new(|| get_env_var("FLE_GCP_PRIVATEKEY"));

// Additional environment variables. These values should be set to the relevant local paths/ports.
#[cfg(feature = "azure-kms")]
static AZURE_IMDS_MOCK_PORT: LazyLock<u16> = LazyLock::new(|| {
    get_env_var("AZURE_IMDS_MOCK_PORT")
        .parse()
        .expect("AZURE_IMDS_MOCK_PORT")
});
static CSFLE_TLS_CERT_DIR: LazyLock<String> = LazyLock::new(|| get_env_var("CSFLE_TLS_CERT_DIR"));
static CRYPT_SHARED_LIB_PATH: LazyLock<String> =
    LazyLock::new(|| get_env_var("CRYPT_SHARED_LIB_PATH"));

fn get_env_var(name: &str) -> String {
    match std::env::var(name) {
        Ok(v) if !v.is_empty() => v,
        _ => {
            panic!(
                "Missing environment variable for {name}. See src/test/csfle.rs for the list of \
                 required variables and instructions for retrieving them."
            )
        }
    }
}

pub(crate) static AWS_KMS: LazyLock<KmsInfo> = LazyLock::new(|| {
    (
        KmsProvider::aws(),
        doc! {
            "accessKeyId": &*FLE_AWS_KEY,
            "secretAccessKey": &*FLE_AWS_SECRET
        },
        None,
    )
});
static AWS_TEMP_KMS: LazyLock<KmsInfo> = LazyLock::new(|| {
    (
        KmsProvider::aws(),
        doc! {
            "accessKeyId": &*FLE_AWS_TEMP_KEY,
            "secretAccessKey": &*FLE_AWS_TEMP_SECRET,
            "sessionToken": &*FLE_AWS_TEMP_SESSION_TOKEN,
        },
        None,
    )
});
pub(crate) static AWS_KMS_NAME1: LazyLock<KmsInfo> = LazyLock::new(|| {
    let aws_info = AWS_KMS.clone();
    (aws_info.0.with_name("name1"), aws_info.1, aws_info.2)
});
pub(crate) static AWS_KMS_NAME2: LazyLock<KmsInfo> = LazyLock::new(|| {
    (
        KmsProvider::aws().with_name("name2"),
        doc! {
            "accessKeyId": &*FLE_AWS_KEY,
            "secretAccessKey": &*FLE_AWS_SECRET
        },
        None,
    )
});
pub(crate) static AZURE_KMS: LazyLock<KmsInfo> = LazyLock::new(|| {
    (
        KmsProvider::azure(),
        doc! {
            "tenantId": &*FLE_AZURE_TENANTID,
            "clientId": &*FLE_AZURE_CLIENTID,
            "clientSecret": &*FLE_AZURE_CLIENTSECRET,
        },
        None,
    )
});
pub(crate) static AZURE_KMS_NAME1: LazyLock<KmsInfo> = LazyLock::new(|| {
    let azure_info = AZURE_KMS.clone();
    (azure_info.0.with_name("name1"), azure_info.1, azure_info.2)
});
pub(crate) static GCP_KMS: LazyLock<KmsInfo> = LazyLock::new(|| {
    (
        KmsProvider::gcp(),
        doc! {
            "email": &*FLE_GCP_EMAIL,
            "privateKey": &*FLE_GCP_PRIVATEKEY,
        },
        None,
    )
});
pub(crate) static GCP_KMS_NAME1: LazyLock<KmsInfo> = LazyLock::new(|| {
    let gcp_info = GCP_KMS.clone();
    (gcp_info.0.with_name("name1"), gcp_info.1, gcp_info.2)
});
pub(crate) static LOCAL_KMS: LazyLock<KmsInfo> = LazyLock::new(|| {
    (
        KmsProvider::local(),
        doc! {
            "key": crate::bson::Binary {
                subtype: crate::bson::spec::BinarySubtype::Generic,
                bytes: base64::decode(&*CSFLE_LOCAL_KEY).unwrap(),
            },
        },
        None,
    )
});
pub(crate) static LOCAL_KMS_NAME1: LazyLock<KmsInfo> = LazyLock::new(|| {
    let local_info = LOCAL_KMS.clone();
    (local_info.0.with_name("name1"), local_info.1, local_info.2)
});
pub(crate) static KMIP_KMS: LazyLock<KmsInfo> = LazyLock::new(|| {
    let cert_dir = PathBuf::from(&*CSFLE_TLS_CERT_DIR);
    let tls_options = TlsOptions::builder()
        .ca_file_path(cert_dir.join("ca.pem"))
        .cert_key_file_path(cert_dir.join("client.pem"))
        .build();
    (
        KmsProvider::kmip(),
        doc! {
            "endpoint": "localhost:5698",
        },
        Some(tls_options),
    )
});
pub(crate) static KMIP_KMS_NAME1: LazyLock<KmsInfo> = LazyLock::new(|| {
    let kmip_info = KMIP_KMS.clone();
    (kmip_info.0.with_name("name1"), kmip_info.1, kmip_info.2)
});

pub(crate) static UNNAMED_KMS_PROVIDERS: LazyLock<KmsProviderList> = LazyLock::new(|| {
    vec![
        AWS_KMS.clone(),
        AZURE_KMS.clone(),
        GCP_KMS.clone(),
        LOCAL_KMS.clone(),
        KMIP_KMS.clone(),
    ]
});
pub(crate) static NAME1_KMS_PROVIDERS: LazyLock<KmsProviderList> = LazyLock::new(|| {
    vec![
        AWS_KMS_NAME1.clone(),
        AZURE_KMS_NAME1.clone(),
        GCP_KMS_NAME1.clone(),
        LOCAL_KMS_NAME1.clone(),
        KMIP_KMS_NAME1.clone(),
    ]
});
pub(crate) static ALL_KMS_PROVIDERS: LazyLock<KmsProviderList> = LazyLock::new(|| {
    let mut providers = UNNAMED_KMS_PROVIDERS.clone();
    providers.extend(NAME1_KMS_PROVIDERS.clone());
    providers.push(AWS_KMS_NAME2.clone());
    providers
});

static EXTRA_OPTIONS: LazyLock<Document> =
    LazyLock::new(|| doc! { "cryptSharedLibPath": &*CRYPT_SHARED_LIB_PATH });
static KV_NAMESPACE: LazyLock<Namespace> =
    LazyLock::new(|| Namespace::from_str("keyvault.datakeys").unwrap());
static DISABLE_CRYPT_SHARED: LazyLock<bool> =
    LazyLock::new(|| env::var("DISABLE_CRYPT_SHARED").is_ok_and(|s| s == "true"));

async fn init_client() -> Result<(EventClient, Collection<Document>)> {
    let client = Client::for_test().monitor_events().await;
    let datakeys = client
        .database("keyvault")
        .collection_with_options::<Document>(
            "datakeys",
            CollectionOptions::builder()
                .read_concern(ReadConcern::majority())
                .write_concern(WriteConcern::majority())
                .build(),
        );
    datakeys.drop().await?;
    client
        .database("db")
        .collection::<Document>("coll")
        .drop()
        .await?;
    Ok((client, datakeys))
}

async fn custom_endpoint_setup(valid: bool) -> Result<ClientEncryption> {
    let update_provider =
        |(provider, mut conf, tls): (KmsProvider, Document, Option<TlsOptions>)| {
            match provider.provider_type() {
                KmsProviderType::Azure => {
                    conf.insert(
                        "identityPlatformEndpoint",
                        if valid {
                            "login.microsoftonline.com:443"
                        } else {
                            "doesnotexist.invalid:443"
                        },
                    );
                }
                KmsProviderType::Gcp => {
                    conf.insert(
                        "endpoint",
                        if valid {
                            "oauth2.googleapis.com:443"
                        } else {
                            "doesnotexist.invalid:443"
                        },
                    );
                }
                KmsProviderType::Kmip => {
                    conf.insert(
                        "endpoint",
                        if valid {
                            "localhost:5698"
                        } else {
                            "doesnotexist.invalid:5698"
                        },
                    );
                }
                _ => (),
            }
            (provider, conf, tls)
        };
    let kms_providers: KmsProviderList = UNNAMED_KMS_PROVIDERS
        .clone()
        .into_iter()
        .map(update_provider)
        .collect();
    Ok(ClientEncryption::new(
        Client::for_test().await.into_client(),
        KV_NAMESPACE.clone(),
        kms_providers,
    )?)
}

async fn validate_roundtrip(
    client_encryption: &ClientEncryption,
    key_id: crate::bson::Binary,
) -> Result<()> {
    let value = RawBson::String("test".to_string());
    let encrypted = client_encryption
        .encrypt(
            value.clone(),
            EncryptKey::Id(key_id),
            Algorithm::Deterministic,
        )
        .await?;
    let decrypted = client_encryption.decrypt(encrypted.as_raw_binary()).await?;
    assert_eq!(value, decrypted);
    Ok(())
}

fn load_testdata_raw(name: &str) -> Result<String> {
    let path: PathBuf = [
        env!("CARGO_MANIFEST_DIR"),
        "../spec/testdata/client-side-encryption",
        name,
    ]
    .iter()
    .collect();
    std::fs::read_to_string(path.clone()).context(path.to_string_lossy().into_owned())
}

fn load_testdata(name: &str) -> Result<Document> {
    Ok(serde_json::from_str(&load_testdata_raw(name)?)?)
}

macro_rules! failure {
    ($($arg:tt)*) => {{
        crate::error::Error::internal(format!($($arg)*)).into()
    }}
}
use failure;

async fn fle2v2_ok(name: &str) -> bool {
    if server_version_lt(7, 0).await {
        log_uncaptured(format!("Skipping {name}: not supported on server < 7.0"));
        return false;
    }
    if topology_is_standalone().await {
        log_uncaptured(format!("Skipping {name}: not supported on standalone"));
        return false;
    }
    true
}

pub(crate) fn fill_kms_placeholders(
    kms_provider_map: std::collections::HashMap<mongocrypt::ctx::KmsProvider, Document>,
) -> KmsProviderList {
    use mongocrypt::ctx::KmsProviderType;

    let placeholder = doc! { "$$placeholder": 1 };

    let mut kms_providers = Vec::new();
    for (provider, mut config) in kms_provider_map {
        // AWS uses temp creds if the "sessionToken" key is present in the config
        let test_kms_provider = if *provider.provider_type() == KmsProviderType::Aws
            && config.contains_key("sessionToken")
        {
            Some(&*AWS_TEMP_KMS)
        } else {
            (*ALL_KMS_PROVIDERS).iter().find(|(p, ..)| p == &provider)
        };

        for (key, value) in config.iter_mut() {
            if value.as_document() == Some(&placeholder) {
                let test_kms_provider =
                    test_kms_provider.unwrap_or_else(|| panic!("missing config for {provider:?}"));
                let placeholder_value = test_kms_provider
                    .1
                    .get(key)
                    .unwrap_or_else(|| panic!("provider config {provider:?} missing key {key:?}"));
                *value = placeholder_value.clone();
            }
        }

        let tls_options = test_kms_provider.and_then(|(_, _, tls_options)| tls_options.clone());
        kms_providers.push((provider, config, tls_options));
    }

    kms_providers
}
